Skip to content

Fix race conditions due to non-volatile lazy vals #2506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 23, 2017

Conversation

smarter
Copy link
Member

@smarter smarter commented May 22, 2017

These races could be observed by running:

dotty-compiler-bootstrapped/test-only *.CompilationTests

These never happened on the CI so far because the order in which the
classes are run is not deterministic and we got "lucky" that the first
tests running the compiler did not use parallelism.

The races were caused by a combination of multiple factors:

  • The new classpath implementation imported from 2.12 has a shared
    global cache of ClassPath instances (see ZipAndJarFileLookupFactory.scala)
  • If multiple threads try to initialize a lazy val in dotty, only one
    will succeed and the other will get null (in scalac, each thread will
    redo the initialization)
  • The classpath implementation relies on code from scala.reflect.io that
    was recently imported into dotty, this code contains fields with lazy vals.

The race with the lazy val in ZipArchive was easy to fix since it
immediately lead to NullPointerException, but the one in AbstractFile
was much harder to find because the only method called on "lazy val
extension" is == which works with null. This suggests that we should
implement a compiler option to check for lazy val races at runtime.

In both cases we fixed the issue by simply dropping the lazy instead
of adding an @volatile, for ZipArchive I cannot think of a usecase
where you would not want to force the lazy val, and for AbstractFile
the cost of always computing the extension is probably less than adding
a lock.

These races could be observed by running:

  dotty-compiler-bootstrapped/test-only *.CompilationTests

These never happened on the CI so far because the order in which the
classes are run is not deterministic and we got "lucky" that the first
tests running the compiler did not use parallelism.

The races were caused by a combination of multiple factors:
- The new classpath implementation imported from 2.12 has a shared
global cache of ClassPath instances (see ZipAndJarFileLookupFactory.scala)
- If multiple threads try to initialize a lazy val in dotty, only one
will succeed and the other will get null (in scalac, each thread will
redo the initialization)
- The classpath implementation relies on code from scala.reflect.io that
was recently imported into dotty, this code contains fields with lazy vals.

The race with the lazy val in ZipArchive was easy to fix since it
immediately lead to NullPointerException, but the one in AbstractFile
was much harder to find because the only method called on "lazy val
extension" is `==` which works with null. This suggests that we should
implement a compiler option to check for lazy val races at runtime.

In both cases we fixed the issue by simply dropping the `lazy` instead
of adding an `@volatile`, for ZipArchive I cannot think of a usecase
where you would not want to force the lazy val, and for `AbstractFile`
the cost of always computing the extension is probably less than adding
a lock.
@smarter
Copy link
Member Author

smarter commented May 22, 2017

See also #2507

@odersky
Copy link
Contributor

odersky commented May 22, 2017

Great detective work!

@DarkDimius
Copy link
Contributor

LGTM

@DarkDimius DarkDimius merged commit d524e28 into scala:master May 23, 2017
smarter referenced this pull request in retronym/scala Jun 9, 2017
Also make the entries of the cache weak references.
@liufengyun
Copy link
Contributor

liufengyun commented Jul 18, 2017

https://liufengyun.github.io/bench/

The benchmarks show that this PR causes a 2x slow down for one test case(more tests are in process).

nicolasstucki added a commit to dotty-staging/dotty that referenced this pull request Jul 26, 2017
As stated in scala#2506 `lazy` was droped to make it volatile.
It turns out that the dirs is not always used and epensive to compute.
In `implicit_cache.scala` only need dirs 1 out of 30 times.
liufengyun added a commit that referenced this pull request Jul 26, 2017
Alternative fix for #2506 that does not force dirs
@retronym
Copy link
Member

@smarter What do you mean by "(in scalac, each thread will redo the initialization" ? Lazy vals initializers that terminate only execute once.

@smarter
Copy link
Member Author

smarter commented Aug 11, 2017

@retronym You're right, I got confused with recursive lazy vals, the difference is that in scalac lazy vals in fields are synchronized by default.

@allanrenucci allanrenucci deleted the fix-races branch December 14, 2017 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants